//将\替换成/
const path = require("path");
const fs = require("fs");
const prettier = require("prettier");
const { SyncHook } = require("tapable")
const parser = require("@babel/parser");
let types = require("@babel/types"); //用来生成或者判断节点的AST语法树的节点
const traverse = require("@babel/traverse").default; //用来遍历AST语法树的节点
const generator = require("@babel/generator").default; //根据AST用来生成代码

//获取文件路径
 function tryExtensions(modulePath,extensions){
  if(fs.existsSync(modulePath)){
    return modulePath;
  }
  for(let i=0;i<extensions.length;i++){
    let filePath = modulePath + extensions[i];
    if(fs.existsSync(filePath)){
      return filePath;
    }
  }
  throw new Error(`${modulePath} is not exist`);
 }

function toUnixPath(filePath) {
  return filePath.replace(/\\/g, "/");
}
function getSource(chunk){
  return `
    (()=>{
      var modules = {
        ${chunk.modules.map(module=>{
          return `"${module.id}":(module) => {
            ${module._source}
          }`
        })}
      };
      
      var cache = {};
      function require(moduleId){
        var cachedModule = cache[moduleId];
        if(cachedModule !== undefined) {
          return cachedModule.exports;
        }
        var module = (cache[moduleId] = {
          exports:{},
        });
        modules[moduleId](module,module.exports,require);

        return module.exports;
      };

      var exports = {};
      ${chunk.entryModule._source}

    })()
  `
}

const baseDir = toUnixPath(process.cwd()); //获取工作目录，在哪里执行命令就获取哪里的目录，这里获取的也是跟操作系统有关系，要替换成/

//Compiler其实是一个类，它是整个编译过程的大管家，而且是单例模式
class Compiler {
  constructor(webpackConfig){
    //存储配置信息
    this.options = webpackConfig;
    //它内部提供了很多钩子
    this.hooks = {
      run: new SyncHook(), //会在编译刚开始的时候触发此run钩子
      done: new SyncHook() //会在编译结束的时候触发此done钩子
    }
  }
  compiler(callback){
    //虽然webpack只有一个Compiler，但是每次编译都会产出一个新的Compilation，
    //这里主要是为了考虑到watch模式，它会在启动时先编译一次，然后监听文件变化，如果发生变化会重新开始编译
    //每次编译都会产出一个新的Compilation，代表每次的编译结果
    let compilation  = new Compilation(this.options);
    compilation.build(callback) //执行compilation的build方法进行编译，编译成功之后执行回调
  }
  //第四步：执行`Compiler`对象的`run`方法开始执行编译
  run(callback){
    //在编译前触发run钩子执行，表示开始启动编译了
    this.hooks.run.call();
    //
    const onCompiled = (err, stats,fileDependencies )=>{
      //第十步：确定好输出内容之后，根据配置的输出路径和文件名，将文件内容写入到文件系统（这里就是硬盘）
      for(let filename in stats.assets){
        let filePath = path.join(this.options.output.path,filename) //拼接出文件的绝对路径
        //输出内容格式化
        prettier.format(stats.assets[filename], { parser: "babel" }).then((result)=>{
          //将编译好的文件内容写入到硬盘上
          fs.writeFileSync(filePath,result,'utf8');
        })
      }
      callback(err,{
        toJson:()=>stats
      });

      fileDependencies.forEach((fileDep)=>{
        fs.watch(fileDep,()=>{
          //如果文件发生变化，重新编译
          this.compiler(onCompiled);
        })
      })
      this.hooks.done.call();//当编译成功后会触发done这个钩子执行
    }
    this.compiler(onCompiled);
  }
}

class Compilation {
  constructor(webpackConfig) {
    this.options = webpackConfig;
    this.modules = [];  //本次编译所有生成出来的模块
    this.chunks = [];  //本次编译产出的所有代码块，入口模块和依赖的模块打包在一起为代码块
    this.assets = {}; //本次编译产出的资源文件
    this.fileDependencies = []; //本次打包涉及到的文件，这里主要是为了实现watch模式下监听文件的变化，文件发生变化后会重新编译
  }
  //当编译模块的时候，name：这个模块是属于哪个代码块chunk的，modulePath：模块绝对路径
  buildModule(name, modulePath){
    //6.2.1 读取模块内容，获取源代码
    let sourceCode = fs.readFileSync(modulePath,"utf8");
    //buildModule最终会返回一个modules模块对象，每个模块都会有一个id,id是相对于根目录的相对路径
    let moduleId = "./" + path.posix.relative(baseDir,modulePath) //模块id:从根目录出发，找到与该模块的相对路径（./src/index.js）
    //创建模块对象
    let module = {
      id: moduleId,
      dependencies: [], //它依赖的模块
      names: [name], //names设计成数组是因为代表的是此模块属于哪个代码块，可能属于多个代码块
      _source: "" ////该模块的代码信息

    }

    // 找到对应的 `Loader` 对源代码进行翻译和替换
    let loaders = [];
    let {rules = []} = this.options.module;
    rules.forEach(rule => {
      const {test} = rule;
      //如果模块的路径和正则匹配，就把此规则对应的loader添加到loader数组中
      if(modulePath.match(test)){
        loaders.push(...rule.use)
      }
    });
    //自右向左对模块进行转译
    sourceCode = loaders.reduceRight((code,loader)=>{
      return loader(code)
    },sourceCode)

    //通过loader翻译后的内容一定得是js内容，因为最后得走我们babel-parse，只有js才能成编译AST
    //第七步：找出此模块所依赖的模块，再对依赖模块进行编译
    //7.1：先把源代码编译成 [AST](https://astexplorer.net/)

    let ast = parser.parse(sourceCode,{sourceType:'module'})
    traverse(ast,{
      CallExpression: (nodePath) => {
        const { node } = nodePath;
        //7.2：在 `AST` 中查找 `require` 语句，找出依赖的模块名称和绝对路径
        if (node.callee.name === 'require') {
          let depModuleName = node.arguments[0].value; //获取依赖的模块
          let dirName = path.posix.dirname(modulePath) //获取当前正在编译的模所在的目录
          let depModulePath = path.posix.join(dirName, depModuleName) //拼接出依赖模块的绝对路径
          let extensions = this.options?.resolve?.extensions || ['.js']; //获取配置中的extensions
          depModulePath = tryExtensions(depModulePath,extensions) //尝试添加后缀名,找到一个真实在硬盘上存在的文件
          //7.3：将依赖模块的绝对路径 push 到 `this.fileDependencies` 中
          this.fileDependencies.push(depModulePath);
          //7.4：生成依赖模块的`模块 id`
          let depModuleId = "./" + path.posix.relative(baseDir,depModulePath);
          //7.5：修改语法结构，把依赖的模块改为依赖`模块 id` require("./name")=>require("./src/name.js")
          node.arguments = [types.stringLiteral(depModuleId)]
          //7.6：将依赖模块的信息 push 到该模块的 `dependencies` 属性中
          module.dependencies.push({depModuleId,depModulePath})
        }
      }
    })

    //7.7：生成新代码，并把转译后的源代码放到 `module._source` 属性上
    let { code } = generator(ast);
    module._source = code
    //7.8：对依赖模块进行编译（对 `module 对象`中的 `dependencies` 进行递归执行 `buildModule` ）
    module.dependencies.forEach(({depModuleId,depModulePath}) =>{
      //考虑到多入口打包 ：一个模块被多个其他模块引用，不需要重复打包
      let existModule = this.modules.find((item)=> item.id === depModuleId)
      //如果modules里已经存在这个将要编译的依赖模块了，那么就不需要编译了，直接把此代码块的名称添加到对应模块的names字段里就可以
      if(existModule) {
        //names指的是它属于哪个代码块chunk
        existModule.names.push(name)
      }else{
        //7.9：对依赖模块编译完成后得到依赖模块的 `module 对象`，push 到 `this.modules` 中
        let depModule = this.buildModule(name,depModulePath)
        this.modules.push(depModule);
      }
    })

    return module
  }

  build(callback) {
    //第五步：根据配置文件中的`entry`配置项找到所有的入口
    let { entry } = this.options;
    if(typeof entry === 'string') {
      entry = {main:entry} //如果是单入口，将entry:"xx"变成{main:"xx"}，这里需要做兼容
    }
    //第六步：从入口文件出发，调用配置的 `loader` 规则，对各模块进行编译
    for(const entryName in entry) {
      let entryFilePath = path.posix.join(baseDir,entry[entryName]) //path.posix为了解决不同操作系统的路径分隔符,这里拿到的就是入口文件的绝对路径
      //6.1 把入口文件的绝对路径添加到依赖数组（`this.fileDependencies`）中，记录此次编译依赖的模块
      this.fileDependencies.push(entryFilePath);
      //6.2 得到入口模块的的 `module` 对象 （里面放着该模块的路径、依赖模块、源代码等）
      let entryModule = this.buildModule(entryName, entryFilePath);
      //6.3 将生成的入口文件 `module` 对象 push 进 `this.modules` 中
      this.modules.push(entryModule);
      //第八步：等所有模块都编译完成后，根据模块之间的依赖关系，组装代码块 `chunk`（一般来说，每个入口文件会对应一个代码块`chunk`，每个代码块`chunk`里面会放着本入口模块和它依赖的模块）
      let chunk = {
        name: entryName, //entryName="main" 代码块的名称
        entryModule, //此代码块对应的module的对象,这里就是src/index.js 的module对象
        modules: this.modules.filter((module) => module.names.includes(entryName)), //找出属于该代码块的模块
      }
      this.chunks.push(chunk); //将代码块push到chunks中

      //第九步：把各个代码块 `chunk` 转换成一个一个文件加入到输出列表
      this.chunks.forEach((chunk)=>{
        let filename = this.options.output.filename.replace("[name]",chunk.name) //这里的[name]会被替换成代码块的名称
        this.assets[filename] = getSource(chunk);
      })
    }
    //这里开始做编译工作，编译成功执行callback
    callback(null,{
      chunks: this.chunks,
      modules: this.modules,
      assets: this.assets,
    },this.fileDependencies);
  }
}

//第一步：搭建结构，读取配置参数，这里接受的是webpack.config.js中的参数
function webpack(webpackConfig) {
  //第二步：用配置参数对象初始化 `Compiler` 对象
  const compiler = new Compiler(webpackConfig);
  //第三步：挂载配置文件中的插件
  const { plugins } = webpackConfig;
  for (const plugin of plugins) {
    //遍历插件数组，调用每个插件的apply方法
    plugin.apply(compiler);
  }
  return compiler;
}

module.exports = webpack;